/*
 * Copyright © 2014-2024 The Android Password Store Authors. All Rights Reserved.
 * SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
 */
package com.github.androidpasswordstore.autofillparser

import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ResolveInfoFlags
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.service.autofill.SaveInfo
import androidx.annotation.RequiresApi

/**
 * In order to add a new browser, do the following:
 * 1. Obtain the .apk from a trusted source. For example, download it from the Play Store on your
 *    phone and use adb pull to get it onto your computer. We will assume that it is called
 *    browser.apk.
 * 2. Run
 *
 * aapt dump badging browser.apk | grep package: | grep -Eo " name='[a-zA-Z0-9_\.]*" | cut -c8-
 *
 * to obtain the package name (actually, the application ID) of the app in the .apk.
 * 3. Run
 *
 * apksigner verify --print-certs browser.apk | grep "#1 certificate SHA-256" | grep -Eo
 * "[a-f0-9]{64}" | tr -d '\n' | xxd -r -p | base64
 *
 * to calculate the hash of browser.apk's first signing certificate. Note: This will only work if
 * the apk has a single signing certificate. Apps with multiple signers are very rare, so there is
 * probably no need to add them. Refer to computeCertificatesHash to learn how the hash would be
 * computed in this case.
 * 4. Verify the package name and the hash, for example by asking other people to repeat the steps
 *    above.
 * 5. Add an entry with the browser apps's package name and the hash to
 *    TRUSTED_BROWSER_CERTIFICATE_HASH.
 * 6. Optionally, try adding the browser's package name to BROWSERS_WITH_SAVE_SUPPORT and check
 *    whether a save request to Password Store is triggered when you submit a registration form.
 * 7. Optionally, try adding the browser's package name to BROWSERS_WITH_MULTI_ORIGIN_SUPPORT and
 *    check whether it correctly distinguishes web origins even if iframes are present on the page.
 *    You can use https://fabianhenneke.github.io/Android-Password-Store/ as a test form.
 *
 * **Security assumption**: Browsers on this list correctly report the web origin of the top-level
 * window as part of their AssistStructure.
 *
 * Note: Browsers can be on this list even if they don't report the correct web origins of all
 * fields on the page, e.g. of those in iframes.
 */
private val TRUSTED_BROWSER_CERTIFICATE_HASH =
  mapOf(
    "com.android.chrome" to arrayOf("8P1sW0EPJcslw7UzRsiXL64w+O50Ed+RBICtay1g24M="),
    "com.brave.browser" to arrayOf("nC23BRNRX9v7vFhbPt89cSPU3GfJT/0wY2HB15u/GKw="),
    "com.chrome.beta" to arrayOf("2mM9NLaeY64hA7SdU84FL8X388U6q5T9wqIIvf0UJJw="),
    "com.chrome.canary" to arrayOf("IBnfofsj779wxbzRRDxb6rBPPy/0Nm6aweNFdjmiTPw="),
    "com.chrome.dev" to arrayOf("kETuX+5LvF4h3URmVDHE6x8fcaMnFqC8knvLs5Izyr8="),
    "com.duckduckgo.mobile.android" to
      arrayOf(
        "u3uzHFc8RqHaf8XFKKas9DIQhFb+7FCBDH8zaU6z0tQ=",
        "8HB9AhwL8+b43MEbo/VwBCXVl9yjAaMeIQVWk067Gwo=",
      ),
    "com.jamal2367.styx" to arrayOf("Lph3oaG1C8WLhLiK5PVxOp5+6wTU9ipJSBYlD2bA3VI="),
    "com.microsoft.emmx" to arrayOf("AeGZlxCoLCdJtNUMRF3IXWcLYTYInQp2anOCfIKh6sk="),
    "com.opera.mini.native" to arrayOf("V6y8Ul8bLr0ZGWzW8BQ5fMkQ/RiEHgroUP68Ph5ZP/I="),
    "com.opera.mini.native.beta" to arrayOf("V6y8Ul8bLr0ZGWzW8BQ5fMkQ/RiEHgroUP68Ph5ZP/I="),
    "com.opera.touch" to arrayOf("qtjiBNJNF3k0yc0MY8xqo4779CxKaVcJfiIQ9X+qZ6o="),
    "org.bromite.bromite" to arrayOf("4e5c0HbXsNyEyytF+3i4bfLrOaO2xWuj3CkqXgw7lQQ="),
    "org.cromite.cromite" to arrayOf("Yz+kHYIR1tCRaoGbiWaMbekuZCMtpn+dFv2Bw7fpI/8="),
    "org.gnu.icecat" to arrayOf("wi2iuVvK/WYZUzd2g0Qzn9ef3kAisQURZ8U1WSMTkcM="),
    "org.mozilla.fenix" to arrayOf("UAR3kIjn+YjVvFzF+HmP6/T4zQhKGypG79TI7krq8hE="),
    "org.mozilla.fenix.nightly" to arrayOf("d+rEzu02r++6dheZMd1MwZWrDNVLrzVdIV57vdKOQCo="),
    "org.mozilla.fennec_aurora" to arrayOf("vASIg40G9Mpr8yOG2qsN2OvPPncweHRZ9i+zzRShuqo="),
    "org.mozilla.fennec_fdroid" to arrayOf("BmZTWO/YugW+I2pHoSywlY19dd2TnXfCsx9TmFN+vcU="),
    "org.mozilla.firefox" to arrayOf("p4tipRZbRJSy/q2edqKA0i2Tf+5iUa7OWZRGsuoxmwQ="),
    "org.mozilla.firefox_beta" to arrayOf("p4tipRZbRJSy/q2edqKA0i2Tf+5iUa7OWZRGsuoxmwQ="),
    "org.mozilla.focus" to arrayOf("YgOkc7421k7jf4f6UA7bx56rkwYQq5ufpMp9XB8bT/w="),
    "org.mozilla.klar" to arrayOf("YgOkc7421k7jf4f6UA7bx56rkwYQq5ufpMp9XB8bT/w="),
    "org.torproject.torbrowser" to arrayOf("IAYfBF5zfGc3XBd5TP7bQ2oDzsa6y3y5+WZCIFyizsg="),
    "org.ungoogled.chromium.stable" to arrayOf("29UOO5cXoxO/e/hH3hOu6bbtg1My4tK6Eik2Ym5Krtk="),
    "org.ungoogled.chromium.extensions.stable" to
      arrayOf("29UOO5cXoxO/e/hH3hOu6bbtg1My4tK6Eik2Ym5Krtk="),
    "com.kiwibrowser.browser" to arrayOf("wGnqlmMy6R4KDDzFd+b1Cf49ndr3AVrQxcXvj9o/hig="),
    "us.spotco.fennec_dos" to
      arrayOf(
        "Jg4KSWeMeLcMAtZTet07bcChcXG73oznX9QCaoo+GNI=",
        "/4H1vlY5ZZTu5w/vKDIlbhUhQSLiupzt0mAF/9S8qqg=",
      ),
    "com.vivaldi.browser" to arrayOf("6KeFRGVbqMCYF/cydo9WibFmLsSyvFoLwOwTjTPKPR4="),
    "app.vanadium.browser" to arrayOf("xq24uDxtTBfSkq/eVv1IilHTFv+PLBHFQQIjv/in27M="),
  )

private fun isTrustedBrowser(context: Context, appPackage: String): Boolean {
  val expectedCertificateHashes = TRUSTED_BROWSER_CERTIFICATE_HASH[appPackage] ?: return false
  val certificateHash = computeCertificatesHash(context, appPackage)
  return certificateHash in expectedCertificateHashes
}

internal enum class BrowserMultiOriginMethod {
  None,
  WebView,
  Field,
}

/**
 * **Security assumption**: Browsers on this list correctly distinguish the web origins of form
 * fields, e.g. on a page which contains both a first-party login form and an iframe with a
 * (potentially malicious) third-party login form.
 *
 * There are two methods used by browsers:
 * - Browsers based on Android's WebView report web domains on each WebView view node, which then
 *   needs to be propagated to the child nodes ( [BrowserMultiOriginMethod.WebView]).
 * - Browsers with custom Autofill implementations report web domains on each input field (
 *   [BrowserMultiOriginMethod.Field]).
 */
private val BROWSER_MULTI_ORIGIN_METHOD =
  mapOf(
    "com.duckduckgo.mobile.android" to BrowserMultiOriginMethod.WebView,
    "com.opera.mini.native" to BrowserMultiOriginMethod.WebView,
    "com.opera.mini.native.beta" to BrowserMultiOriginMethod.WebView,
    "com.opera.touch" to BrowserMultiOriginMethod.WebView,
    "org.gnu.icecat" to BrowserMultiOriginMethod.WebView,
    "org.mozilla.fenix" to BrowserMultiOriginMethod.Field,
    "org.mozilla.fenix.nightly" to BrowserMultiOriginMethod.Field,
    "org.mozilla.fennec_aurora" to BrowserMultiOriginMethod.Field,
    "org.mozilla.fennec_fdroid" to BrowserMultiOriginMethod.Field,
    "org.mozilla.firefox" to BrowserMultiOriginMethod.WebView,
    "org.mozilla.firefox_beta" to BrowserMultiOriginMethod.WebView,
    "org.mozilla.focus" to BrowserMultiOriginMethod.Field,
    "org.mozilla.klar" to BrowserMultiOriginMethod.Field,
    "org.torproject.torbrowser" to BrowserMultiOriginMethod.WebView,
    "us.spotco.fennec_dos" to BrowserMultiOriginMethod.Field,
  )

private fun getBrowserMultiOriginMethod(appPackage: String): BrowserMultiOriginMethod =
  BROWSER_MULTI_ORIGIN_METHOD[appPackage] ?: BrowserMultiOriginMethod.None

/**
 * Browsers on this list issue Autofill save requests and provide unmasked passwords as
 * `autofillValue`.
 *
 * Some browsers may not issue save requests automatically and thus need
 * `FLAG_SAVE_ON_ALL_VIEW_INVISIBLE` to be set.
 */
@RequiresApi(Build.VERSION_CODES.O)
private val BROWSER_SAVE_FLAG =
  mapOf(
    "com.duckduckgo.mobile.android" to 0,
    "org.mozilla.klar" to 0,
    "org.mozilla.focus" to 0,
    "org.mozilla.fenix" to 0,
    "org.mozilla.fenix.nightly" to 0,
    "org.mozilla.fennec_aurora" to 0,
    "com.opera.mini.native" to 0,
    "com.opera.mini.native.beta" to 0,
    "com.opera.touch" to 0,
  )

@RequiresApi(Build.VERSION_CODES.O)
private val BROWSER_SAVE_FLAG_IF_NO_ACCESSIBILITY =
  mapOf(
    "com.android.chrome" to SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE,
    "com.chrome.beta" to SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE,
    "com.chrome.canary" to SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE,
    "com.chrome.dev" to SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE,
    "org.bromite.bromite" to SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE,
    "org.cromite.cromite" to SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE,
    "org.ungoogled.chromium.stable" to SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE,
  )

private fun isNoAccessibilityServiceEnabled(context: Context): Boolean {
  // See https://chromium.googlesource.com/chromium/src/+/447a31e977a65e2eb78804e4a09633699b4ede33
  return Settings.Secure.getString(
      context.contentResolver,
      Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
    )
    .isNullOrEmpty()
}

@RequiresApi(Build.VERSION_CODES.O)
private fun getBrowserSaveFlag(context: Context, appPackage: String): Int? =
  BROWSER_SAVE_FLAG[appPackage]
    ?: BROWSER_SAVE_FLAG_IF_NO_ACCESSIBILITY[appPackage]?.takeIf {
      isNoAccessibilityServiceEnabled(context)
    }

internal data class BrowserAutofillSupportInfo(
  val multiOriginMethod: BrowserMultiOriginMethod,
  val saveFlags: Int?,
)

@RequiresApi(Build.VERSION_CODES.O)
internal fun getBrowserAutofillSupportInfoIfTrusted(
  context: Context,
  appPackage: String,
): BrowserAutofillSupportInfo? {
  if (!isTrustedBrowser(context, appPackage)) return null
  return BrowserAutofillSupportInfo(
    multiOriginMethod = getBrowserMultiOriginMethod(appPackage),
    saveFlags = getBrowserSaveFlag(context, appPackage),
  )
}

private val FLAKY_BROWSERS = listOf("com.kiwibrowser.browser")

public enum class BrowserAutofillSupportLevel {
  None,
  FlakyFill,
  PasswordFill,
  PasswordFillAndSaveIfNoAccessibility,
  GeneralFill,
  GeneralFillAndSave,
}

@RequiresApi(Build.VERSION_CODES.O)
private fun getBrowserAutofillSupportLevel(
  context: Context,
  appPackage: String,
): BrowserAutofillSupportLevel {
  val browserInfo = getBrowserAutofillSupportInfoIfTrusted(context, appPackage)
  return when {
    browserInfo == null -> BrowserAutofillSupportLevel.None
    appPackage in FLAKY_BROWSERS -> BrowserAutofillSupportLevel.FlakyFill
    appPackage in BROWSER_SAVE_FLAG_IF_NO_ACCESSIBILITY ->
      BrowserAutofillSupportLevel.PasswordFillAndSaveIfNoAccessibility
    browserInfo.multiOriginMethod == BrowserMultiOriginMethod.None ->
      BrowserAutofillSupportLevel.PasswordFill
    browserInfo.saveFlags == null -> BrowserAutofillSupportLevel.GeneralFill
    else -> BrowserAutofillSupportLevel.GeneralFillAndSave
  }.takeUnless { supportLevel ->
    // On Android Oreo, only browsers with native Autofill support can be used with Password
    // Store
    // (compatibility mode is only available on Android Pie and higher). Since all known
    // browsers
    // with native Autofill support offer full save support as well, we reuse the list of those
    // browsers here.
    supportLevel != BrowserAutofillSupportLevel.GeneralFillAndSave &&
      Build.VERSION.SDK_INT < Build.VERSION_CODES.P
  } ?: BrowserAutofillSupportLevel.None
}

@RequiresApi(Build.VERSION_CODES.O)
public fun getInstalledBrowsersWithAutofillSupportLevel(
  context: Context
): List<Pair<String, BrowserAutofillSupportLevel>> {
  val testWebIntent = Intent(Intent.ACTION_VIEW).apply { data = Uri.parse("https://example.org") }
  val installedBrowsers =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
      context.packageManager.queryIntentActivities(
        testWebIntent,
        ResolveInfoFlags.of(PackageManager.MATCH_ALL.toLong()),
      )
    } else {
      context.packageManager.queryIntentActivities(testWebIntent, PackageManager.MATCH_ALL)
    }
  return installedBrowsers
    .map { it to getBrowserAutofillSupportLevel(context, it.activityInfo.packageName) }
    .filter { it.first.isDefault || it.second != BrowserAutofillSupportLevel.None }
    .map {
      context.packageManager
        .getApplicationLabel(it.first.activityInfo.applicationInfo)
        .toString() to it.second
    }
}
